/**
 * Copyright 2009 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.xume.raob;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

import org.xume.raob.creators.ClassBasedObjectCreator;
import org.xume.raob.extensions.ExtensionMethod;
import org.xume.raob.extensions.ExtensionMethodIntrospectionResult;
import org.xume.raob.statements.Statement;
import org.xume.raob.statements.StatementsFactory;
import org.xume.raob.statements.StatementsFactoryIntrospectionResult;
import org.xume.raob.util.ArrayUtils;
import org.xume.raob.util.ReflectionUtils;

/**
 * @author Johan Siebens
 */
@SuppressWarnings("unchecked")
public class ObjectBuilderFactory {

	private static final Method BUILD_METHOD = buildMethod();

	public static <T, V extends ObjectBuilder<T>> V createBuilder(Class<V> builderInterface) {
		Class<T> targetClass = ReflectionUtils.targetClass(builderInterface);
		ClassBasedObjectCreator<T> creator = new ClassBasedObjectCreator<T>(targetClass);
		return createBuilder(creator, builderInterface);
	}

	public static <T, V extends ObjectBuilder<T>> V createBuilder(ObjectCreator<T> objectCreator, Class<V> builderInterface) {
		ExtensionMethodIntrospectionResult extensions = ExtensionMethodIntrospectionResult.forBuilder(builderInterface);
		StatementsFactoryIntrospectionResult statementsFactories = StatementsFactoryIntrospectionResult.forBuilder(builderInterface);
		ObjectBuilderHandler<T> handler = new ObjectBuilderHandler<T>(objectCreator, statementsFactories, extensions);
		V builder = (V) Proxy.newProxyInstance(builderInterface.getClassLoader(), new Class[] { builderInterface }, handler);
		return builder;
	}

	private static Method buildMethod() {
		try {
			return ObjectBuilder.class.getDeclaredMethod("build");
		} catch (Exception e) {
			throw new IllegalStateException(e);
		}
	}

	private static class ObjectBuilderHandler<T> implements java.lang.reflect.InvocationHandler {

		private final ExtensionMethodIntrospectionResult extensions;

		private final ObjectCreator<T> objectCreator;

		private final List<Statement> statements = new LinkedList<Statement>();

		private final StatementsFactoryIntrospectionResult statementsFactories;

		public ObjectBuilderHandler(ObjectCreator<T> objectCreator, StatementsFactoryIntrospectionResult statementsFactories,
				ExtensionMethodIntrospectionResult extensions) {
			this.objectCreator = objectCreator;
			this.statementsFactories = statementsFactories;
			this.extensions = extensions;
		}

		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			Object result = null;

			if (BUILD_METHOD.equals(method)) {
				result = buildObject();
			} else if (statementsFactories.contains(method)) {
				StatementsFactory<?> statementsFactory = statementsFactories.get(method);
				statements.addAll(Arrays.asList(statementsFactory.getStatements(args)));
				result = proxy;
			} else if (extensions.contains(method)) {
				Object[] actualArgs = ArrayUtils.addFirst(args, buildObject());
				ExtensionMethod extensionsMethod = extensions.get(method);
				result = extensionsMethod.execute(actualArgs);
			} else {
				throw new UnsupportedOperationException("Don't know how to execute method [" + method + "]");
			}

			return result;
		}

		private Object buildObject() throws Exception {
			T result = objectCreator.createNewObject();
			for (Statement statement : statements) {
				statement.execute(result);
			}
			return result;
		}

	}

}
